#include "lua_helper.hh"
#include <sstream>
#include <vector>
#include <string>
#include "../../corelib/util/string_util.hh"
#include "../../plugin/base/rpc_base.hh"
#include "lua_framework.hh"
#include "lua_bind.hh"

int LUA_CO_KEY;

std::string getStackInfo(lua_State* L) {
    auto vm = LuaRef.getVirtualMachine();
    std::string stackTrace;
    auto top = lua_gettop(vm);
    luaL_traceback(vm, L, nullptr, 0);
    stackTrace += "\nLua runtime error:\n\t";
    stackTrace += lua_tostring(L, -1);
    stackTrace += "\n";
    stackTrace += lua_tostring(vm, -1);
    lua_pop(vm, lua_gettop(vm) - top);
    lua_settop(L, 0);
    return stackTrace;
}

bool catchError(lua_State* L, int status) {
    switch (status) {
        case LUA_OK:
        case LUA_YIELD:
            return true;
        case LUA_ERRRUN:
        case LUA_ERRERR:
            errorlog << getStackInfo(L) << endlog;
            break;
        default:
            break;
    }
    // Cleanup stack
    lua_settop(L, 0);
    return false;
}

void raiseError(lua_State * L, const char * msg) {
    lua_pushstring(L, msg);
    lua_error(L);
}

void toLua(lua_State * co, const google::protobuf::Message & msg) {
    auto desc = msg.GetDescriptor();
    auto ref = msg.GetReflection();
    lua_newtable(co);
    for (int i = 0; i < desc->field_count(); ++i) {
        const google::protobuf::FieldDescriptor* field = desc->field(i);
        if (!field->is_repeated() && !ref->HasField(msg, field)) {
            continue;
        }
        else {
            // For existing field
            onField(co, msg, field);
        }
    }
}

void copyTableLuaToC(lua_State * co, google::protobuf::Message & msg) {
    auto desc = msg.GetDescriptor();
    auto ref = msg.GetReflection();
    lua_pushnil(co);
    while (lua_next(co, -2)) {
        if (!lua_isstring(co, -2)) {
            lua_pop(co, 1);
            continue;
        }
        auto key = lua_tostring(co, -2);
        auto fieldDesc = desc->FindFieldByName(key);
        if (fieldDesc) {
            onLuaTableValue(co, msg, ref, fieldDesc, -1);
        }
        else {
            // error
            errorlog << "Field not found in " << msg.GetDescriptor()->name() << " with '" << key << "'" << endlog;
        }
        lua_pop(co, 1);
    }
}

void onRepeatedField(lua_State * co, const google::protobuf::Message & msg, const google::protobuf::FieldDescriptor * field) {
    auto count = msg.GetReflection()->FieldSize(msg, field);
    if (field->cpp_type() == google::protobuf::FieldDescriptor::Type::TYPE_MESSAGE) {
        for (auto i = 0; i < count; i++) {
            auto& curMsg = msg.GetReflection()->GetRepeatedMessage(msg, field, i);
            toLua(co, curMsg);
        }
    }
    else {
        for (auto i = 0; i < count; i++) {
            switch (field->type()) {
            case google::protobuf::FieldDescriptor::Type::TYPE_BOOL: {
                auto value = msg.GetReflection()->GetRepeatedBool(msg, field, i);
                lua_pushboolean(co, value);
                lua_pushinteger(co, (lua_Integer)i);
                lua_settable(co, -3);
                break;
            }
            case google::protobuf::FieldDescriptor::Type::TYPE_INT32: {
                auto value = msg.GetReflection()->GetRepeatedInt32(msg, field, i);
                lua_pushinteger(co, (lua_Integer)value);
                lua_pushinteger(co, (lua_Integer)i);
                lua_settable(co, -3);
                break;
            }
            case google::protobuf::FieldDescriptor::Type::TYPE_INT64: {
                auto value = msg.GetReflection()->GetRepeatedInt64(msg, field, i);
                lua_pushinteger(co, (lua_Integer)value);
                lua_pushinteger(co, (lua_Integer)i);
                lua_settable(co, -3);
                break;
            }
            case google::protobuf::FieldDescriptor::Type::TYPE_UINT32: {
                auto value = msg.GetReflection()->GetRepeatedUInt32(msg, field, i);
                lua_pushinteger(co, (lua_Integer)value);
                lua_pushinteger(co, (lua_Integer)i);
                lua_settable(co, -3);
                break;
            }
            case google::protobuf::FieldDescriptor::Type::TYPE_UINT64: {
                auto value = msg.GetReflection()->GetRepeatedUInt64(msg, field, i);
                lua_pushinteger(co, (lua_Integer)value);
                lua_pushinteger(co, (lua_Integer)i);
                lua_settable(co, -3);
                break;
            }
            case google::protobuf::FieldDescriptor::Type::TYPE_FLOAT: {
                auto value = msg.GetReflection()->GetRepeatedFloat(msg, field, i);
                lua_pushnumber(co, (lua_Number)value);
                lua_pushinteger(co, (lua_Integer)i);
                lua_settable(co, -3);
                break;
            }
            case google::protobuf::FieldDescriptor::Type::TYPE_DOUBLE: {
                auto value = msg.GetReflection()->GetRepeatedDouble(msg, field, i);
                lua_pushnumber(co, (lua_Number)value);
                lua_pushinteger(co, (lua_Integer)i);
                lua_settable(co, -3);
                break;
            }
            case google::protobuf::FieldDescriptor::Type::TYPE_ENUM: {
                auto value = msg.GetReflection()->GetRepeatedEnum(msg, field, i);
                auto num = value->number();
                lua_pushnumber(co, (lua_Number)num);
                lua_pushinteger(co, (lua_Integer)i);
                lua_settable(co, -3);
                break;
            }
            case google::protobuf::FieldDescriptor::Type::TYPE_STRING: {
                auto value = msg.GetReflection()->GetRepeatedString(msg, field, i);
                lua_pushstring(co, value.c_str());
                lua_pushinteger(co, (lua_Integer)i);
                lua_settable(co, -3);
                break;
            }
            }
        }
    }
}

void onField(lua_State * co, const google::protobuf::Message & msg, const google::protobuf::FieldDescriptor * field) {
    if (field->is_repeated()) {
        lua_newtable(co);
        onRepeatedField(co, msg, field);
        lua_setfield(co, -2, field->name().c_str());
    }
    else {
        switch (field->type()) {
        case google::protobuf::FieldDescriptor::Type::TYPE_BOOL:
            pushBoolToLua(co, msg, field);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_INT32:
            pushInt32ToLua(co, msg, field);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_INT64:
            pushInt64ToLua(co, msg, field);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_UINT32:
            pushUInt32ToLua(co, msg, field);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_UINT64:
            pushUInt64ToLua(co, msg, field);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_FLOAT:
            pushFloatToLua(co, msg, field);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_DOUBLE:
            pushDoubleToLua(co, msg, field);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_ENUM:
            pushEnumToLua(co, msg, field);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_STRING:
            pushStringToLua(co, msg, field);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_MESSAGE: {
            auto& curMsg = msg.GetReflection()->GetMessage(msg, field);
            toLua(co, curMsg);
            lua_setfield(co, -2, field->name().c_str());
            break;
        }
        }
    }
}

void onLuaRepeatedValue(lua_State * co, google::protobuf::Message & msg, const google::protobuf::Reflection * ref, const google::protobuf::FieldDescriptor * field, int index) {
    lua_pushnil(co);
    while (lua_next(co, -2)) {
        switch (field->type()) {
        case google::protobuf::FieldDescriptor::Type::TYPE_BOOL:
            addFieldBool(co, msg, ref, field, index);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_INT32:
            addFieldInt32(co, msg, ref, field, index);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_INT64:
            addFieldInt64(co, msg, ref, field, index);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_UINT32:
            addFieldUInt32(co, msg, ref, field, index);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_UINT64:
            addFieldUInt64(co, msg, ref, field, index);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_FLOAT:
            addFieldFloat(co, msg, ref, field, index);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_DOUBLE:
            addFieldDouble(co, msg, ref, field, index);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_ENUM:
            addFieldEnum(co, msg, ref, field, index);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_STRING:
            addFieldString(co, msg, ref, field, index);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_MESSAGE: {
            auto innerMsg = ref->AddMessage(&msg, field);
            copyTableLuaToC(co, *innerMsg);
            break;
        }
        }
        lua_pop(co, 1);
    }
}

void onLuaTableValue(lua_State * co, google::protobuf::Message & msg, const google::protobuf::Reflection * ref, const google::protobuf::FieldDescriptor * field, int index) {
    if (field->is_repeated()) {
        onLuaRepeatedValue(co, msg, ref, field, index);
    }
    else {
        switch (field->type()) {
        case google::protobuf::FieldDescriptor::Type::TYPE_BOOL:
            setFieldBool(co, msg, ref, field, index);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_INT32:
            setFieldInt32(co, msg, ref, field, index);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_INT64:
            setFieldInt64(co, msg, ref, field, index);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_UINT32:
            setFieldUInt32(co, msg, ref, field, index);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_UINT64:
            setFieldUInt64(co, msg, ref, field, index);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_FLOAT:
            setFieldFloat(co, msg, ref, field, index);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_DOUBLE:
            setFieldDouble(co, msg, ref, field, index);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_ENUM:
            setFieldEnum(co, msg, ref, field, index);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_STRING:
            setFieldString(co, msg, ref, field, index);
            break;
        case google::protobuf::FieldDescriptor::Type::TYPE_MESSAGE: {
            auto innerMsg = ref->MutableMessage(&msg, field);
            copyTableLuaToC(co, *innerMsg);
            break;
        }
        }
    }
}

void onCallAck(lua_State * co, const google::protobuf::Message & msg) {
    toLua(co, msg);
    catchError(co, lua_resume(co, nullptr, 1));
}

void onCallTimeout(lua_State * co) {
    lua_pushnil(co);
    catchError(co, lua_resume(co, nullptr, 0));
}

void installCModule(lua_State * L, const char * name, const luaL_Reg * clibFunc) {
    luaL_register(L, name, clibFunc);
    lua_pushvalue(L, -1);
    lua_setfield(L, -2, "__index");
}

bool installRPCMethod(lua_State * L, const std::string & callPath, lua_CFunction cfunc) {
    StackBalancer sb(L);
    std::vector<std::string> result;
    kratos::corelib::StringUtil::split(callPath, ".", result);
    // install method path table and last method
    if (result.empty()) {
        errorlog << "Invalid argument to install RPC method:" << callPath << endlog;
        return false;
    }
    if (result.size() < 2) {
        // Not for install the global method
        errorlog << "Invalid argument to install RPC method:" << callPath << endlog;
        return false;
    }
    auto& globalName = result[0];
    lua_getglobal(L, globalName.c_str());
    if (lua_isnoneornil(L, -1)) {
        // New global variable
        lua_pop(L, 1);
        lua_newtable(L);
        lua_setglobal(L, globalName.c_str());
        lua_getglobal(L, globalName.c_str());
    } else {
        // Existing global variable
        if (!lua_istable(L, -1)) {
            errorlog << "The element in path already installed but with invalid type:" << callPath << endlog;
            return false;
        }
    }
    for (std::size_t i = 1; i < result.size() - 1; i++) {
        auto& tableName = result[i];
        lua_pushstring(L, tableName.c_str());
        lua_gettable(L, -2);
        if (lua_isnoneornil(L, -1)) {
            lua_pop(L, 1);
            lua_pushstring(L, tableName.c_str());
            lua_newtable(L);
            lua_settable(L, -3);
            lua_pushstring(L, tableName.c_str());
            lua_gettable(L, -2);
            lua_remove(L, -2);
        } else {
            // Existing variable
            if (!lua_istable(L, -1)) {
                errorlog << "The element in path already installed but with invalid type:" << callPath << endlog;
                return false;
            }
        }
    }
    auto& methodName = result.back();
    lua_pushstring(L, methodName.c_str());
    lua_gettable(L, -2);
    if (lua_isnoneornil(L, -1)) {
        lua_pop(L, 1);
        lua_pushstring(L, methodName.c_str());
        lua_pushcfunction(L, cfunc);
        lua_settable(L, -3);
    } else {
        // Existing variable
        if (!lua_iscfunction(L, -1)) {
            errorlog << "The element in path already installed but with invalid type:" << callPath << endlog;
            return false;
        }
    }
    return true;
}

void uninstallRPCMethod(lua_State* L, const std::string& interface) {
    StackBalancer sb(L);
    lua_getglobal(L, interface.c_str());
    lua_pushnil(L);
    lua_settable(L, -2);
}

void installCoTable(lua_State * L) {
    lua_pushlightuserdata(L, &LUA_CO_KEY);
    lua_createtable(L, 0, 32);
    lua_rawset(L, LUA_REGISTRYINDEX);
}

CoInfo * CoInfo::newThread() {
    CoInfo* coi = nullptr;
    auto L = LuaRef.getVirtualMachine();
    auto top = lua_gettop(L);
    lua_pushlightuserdata(L, &LUA_CO_KEY);
    lua_rawget(L, LUA_REGISTRYINDEX);
    auto co = lua_newthread(L);
    auto ref = luaL_ref(L, -2);
    lua_settop(L, top);
    if (ref == LUA_NOREF) {
        return nullptr;
    }
    coi = kratos::corelib::allocate<CoInfo>();
    coi->ref_ = ref;
    coi->co_ = co;
    return coi;
}

void CoInfo::deleteThread(CoInfo* coi) {
    kratos::corelib::dispose(coi);
}

CoInfo::CoInfo() {
    ref_ = 0;
    co_ = nullptr;
}

CoInfo::~CoInfo() {
    clear();
}

void CoInfo::clear() {
    if (co_) {
        auto L = LuaRef.getVirtualMachine();
        luaL_unref(L, LUA_REGISTRYINDEX, ref_);
    }
    co_ = nullptr;
    ref_ = 0;
}
